Only re-dump package metadata when its dumpable content changed - #1803
Open
Seldaek wants to merge 11 commits into
Open
Only re-dump package metadata when its dumpable content changed#1803Seldaek wants to merge 11 commits into
Seldaek wants to merge 11 commits into
Conversation
The Updater now nulls dumpedAt/dumpedAtV2 precisely when a crawl actually
changes dumpable content — a version created, mutated, auto-recovered or
auto-soft-deleted, or the package's abandoned/default-branch/source-dist state
changing (all of which surface as a VersionUpdatedResult) — instead of leaving
re-dump detection solely to a crawledAt bump. An unchanged re-crawl no longer
marks the package for a fresh metadata dump.
VersionRepository::remove() nulls the dump timestamps directly (covering the
prune loop, delete-before, clear-versions and whole-package deletion), and
softDelete()/recover() now schedule their update with force_dump so the
removed/restored version is reflected in the dumped files.
The crawledAt-based staleness selection is unchanged for now, so behaviour is a
strict superset. A metadata_dump.file{forced} tag is added so we can confirm
every content change marks the package for re-dump before that fallback is
eventually dropped.
glaubinix
reviewed
Aug 3, 2026
glaubinix
approved these changes
Aug 4, 2026
…key, this allows more similar jobs to be queued but it ensures force-dump ones go through
glaubinix
approved these changes
Aug 4, 2026
Conflicts in the version soft-delete path, against composer#1814 which lets admins hide an already soft-deleted version: - VersionRepository::softDelete() keeps main's $wasSoftDeleted early return, and places it before this branch's markForDump(). A reason change leaves the dumped bytes untouched — the version was already excluded — so it must not request a re-dump either, same reasoning as the remove() guard here. - The test file conflict was the import block only; took the union.
The merge with composer#1814 created the case: an admin switching an already soft-deleted version to Hidden restamps and audits it, but the dumped metadata never contained the version, so nothing may mark the package stale.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Re-dumping v2 metadata used to be driven almost entirely by
crawledAt: any crawl re-staled the package, so the monthly "crawl all" flooded the dumper with packages whose files had not changed. This makes re-dumps content-driven instead — a crawl marks the package only when it actually changed dumpable content: a version created or mutated (which also covers the default-branch toggle, the source/dist URL rewrite and the abandoned-state change, all of which surface as aVersionUpdatedResult), an auto-recovered version, or an auto-soft-deleted one. An unchanged re-crawl no longer marks anything.Marking is non-destructive
Marking used to null
dumpedAtV2, which loses requests:V2Dumper::dump()recordsdumpedAtV2for a whole batch only at the end, so a mark landing between a package's hydration and that write was silently overwritten.crawledAtmasked that for the Updater, butSecurityAdvisoryUpdateListenerandFilterListEntryUpdateListenernever touchcrawledAt— so a newly published advisory could stay missing fromp2/until an unrelated crawl. That was a live bug, and invisible to any file-level metric since no file is written.A new
package.dumpRequestedAtcolumn records the request instead, and staleness becomesdumpedAtV2 IS NULL OR dumpRequestedAt >= dumpedAtV2.$dumpTimeis captured before hydration, so a mark landing mid-run leaves the package stale and it is picked up next run.>=rather than>because both columns hold whole seconds: a same-second mark is ambiguous and must resolve towards re-dumping.Two levels of request:
Package::markForDump()— "re-dump if the content changed". Every ordinary content-change path uses this;writeV2File()still compares content and early-returns if it matches.Package::forceDump()— additionally nullsdumpedAtV2, whichwriteV2File()reads as "write this regardless", re-uploading byte-identical files to bust the CDN cache and re-run the replica write. Reserved for the deliberate "make it publish again" paths:Updater::FORCE_DUMP(manual update button, unfreeze, repo URL rewrite) andPackage::unfreeze(). It always marks as well, so an in-flight dump run can at worst cost the forcing, never the request.VersionRepository::softDelete()/recover()now mark directly rather than relying on the scheduled crawl: pulling a version is the security path, and a package whose repository 404s never reaches the end ofUpdater::update(), which would leave a pulled version published indefinitely.remove()marks directly too, but not for rows that were already soft-deleted — those were excluded from the dumped metadata already, so hard-purging them changes no bytes.Scheduler::scheduleUpdate()keys its dedup on(packageId, update_source_dist_url, delete_before), so a pending plain job used to swallow aforce_dumprequest entirely. It now carries the intent both ways: onto an already-queued job, and onto the replacement job when a scheduled-for-later job is cancelled in favour of an immediate one.Convergence fixes in the Updater
updateInformation()rebuilt a dev version on every crawl, forever, whenever its rebuild trigger could not agree with what the reconciliation stores — bumpingVersion::updatedAt(=published-timefor dev rows), i.e. a real CDN upload + replica write + purge each time. Three causes, all fixed:sanitize()alters could never match. Both sides go throughnormalizeReplacementPackage()now;Version::toV2Array()reads it off the package at dump time.Instrumentation and the transitional net
The
crawledAtstaleness clause is deliberately unchanged, so selection remains a strict superset and there is no regression. Dropping it is a follow-up, gated on a new gap-detector:metadata_dump.file{result:written, requested:false}counts content changes that onlycrawledAtcaught, i.e. a change path that fails to mark, and must sit at ~0 first. Its inverse{result:skipped, requested:true}catches paths that mark more eagerly than needed.result:createdsplits out first writes of a missing local file so they do not pollute the gate, and the legacyfilemtime >= 1606210609re-dump branch — which re-verified any p2 file whose mtime predates 2020-11-24 — is removed for the same reason.Schema
Two migrations, one on each side of the deploy, because the stale query pins its index by name with
USE INDEX:2026_08_package_dump_requested_at.sql— before the deploy: addsdumpRequestedAtanddumped2_requested_crawled_frozen_idx.2026_08b_drop_dead_dump_columns.sql— after the deploy: dropsdumped_idx, the dead v1dumpedAtcolumn (the v1 dumper is gone and its readers had no callers), and the supersededdumped2_crawled_frozen_idx.